/*
*Copyright (c) 2005-2013, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
*WSO2 Inc. licenses this file to you under the Apache License,
*Version 2.0 (the "License"); you may not use this file except
*in compliance with the License.
*You may obtain a copy of the License at
*
*http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing,
*software distributed under the License is distributed on an
*"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
*KIND, either express or implied.  See the License for the
*specific language governing permissions and limitations
*under the License.
*/

package org.wso2.carbon.identity.oauth;

import com.google.gdata.client.authn.oauth.GoogleOAuthParameters;
import com.google.gdata.client.authn.oauth.OAuthException;
import com.google.gdata.client.authn.oauth.OAuthHmacSha1Signer;
import com.google.gdata.client.authn.oauth.OAuthUtil;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.ServiceContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.core.common.AuthenticationException;
import org.wso2.carbon.identity.base.IdentityException;
import org.wso2.carbon.identity.core.model.OAuthAppDO;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.oauth.dao.OAuthAppDAO;
import org.wso2.carbon.identity.oauth.dao.OAuthConsumerDAO;
import org.wso2.carbon.identity.oauth.dto.OAuthConsumerDTO;
import org.wso2.carbon.registry.core.exceptions.RegistryException;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

public class OAuthService {

    private static Log log = LogFactory.getLog(OAuthService.class);

	private static final String OAUTH_LATEST_TIMESTAMP = "OAUTH_LATEST_TIMESTAMP";
	private static final String OAUTH_NONCE_STORE = "OAUTH_NONCE_STORE";

	/**
	 * Checks whether the given consumer is valid or not. This is done by validating the signature,
	 * signed by this particular consumer.
	 * 
	 * @param oauthConsumer Parameter related to the OAuth authorization header.
	 * @return
	 * @throws Exception
	 */
	public boolean isOAuthConsumerValid(OAuthConsumerDTO oauthConsumer) throws IdentityException {

        String oAuthSecretKey = getOAuthSecretKey(oauthConsumer.getOauthConsumerKey());

        if(oAuthSecretKey == null){
            log.debug("Invalid Consumer Key.");
            throw new IdentityException("Invalid Consumer Key");
        }
        try{
            return validateOauthSignature(oauthConsumer,
                    oAuthSecretKey);
        }catch (AuthenticationException e){
            throw new IdentityException(e.getMessage(),e);
        }
	}

	/**
	 * Returns the OAuth request token after verifying the consumer. The Service Provider verifies
	 * the signature and Consumer Key. If successful, it generates a Request Token and Token Secret
	 * and returns them to the Consumer in the HTTP response body as defined in Service Provider
	 * Response Parameters. The Service Provider MUST ensure the Request Token cannot be exchanged
	 * for an Access Token until the User successfully grants access in Obtaining User
	 * Authorization.
	 * 
	 * @param params A container for the following attributes.
	 * @param params:oauth_consumer_key (required) : Domain identifying the third-party web
	 *            application. This is the domain used when registering the application with WSO2
	 *            Identity Server.
	 * @param params:oauth_nonce (required) : Random 64-bit, unsigned number encoded as an ASCII
	 *            string in decimal format. The nonce/time-stamp pair should always be unique to
	 *            prevent replay attacks.
	 * @param params:oauth_signature_method (required) : Signature algorithm. The legal values for
	 *            this parameter "RSA-SHA1" or "HMAC-SHA1". WSO2 does not support "PLAINTEXT" and
	 *            "RSA-SHA1".
	 * @param params:oauth_signature (required) String generated using the referenced signature
	 *            method.
	 * @param params:oauth_timestamp (required) : Integer representing the time the request is sent.
	 *            The time-stamp should be expressed in number of seconds after January 1, 1970
	 *            00:00:00 GMT. scope (required) URL identifying the service(s) to be accessed. The
	 *            resulting token enables access to the specified service(s) only.
	 * @param params:scope : The resource the third party web application should be authorized to
	 *            access. To specify more than one scope, list each one separated with a space. This
	 *            parameter is not defined in the OAuth standards, it is a WSO2-specific parameter.
	 * @param params:oauth_callback (required) : URL the user should be redirected to after access
	 *            to a service is granted (in response to a call to OAuthAuthorizeToken). The
	 *            response to this getOauthRequestToken call verifies that WSO2 handles a call-back
	 *            URL.
	 * @param params:oauth_version (optional) : The OAuth version used by the requesting web
	 *            application. This value must be "1.0"; if not provided, WSO2 assumes version 1.0
	 *            is in use.
	 * @return oauth_token, oauth_token_secret, oauth_callback_confirmed
	 * @throws Exception
	 */
	public Parameters getOauthRequestToken(Parameters params) throws AuthenticationException, IdentityOAuthAdminException {

		boolean isValidSignature = false;
		String secretkey = null;

		validateTimestampAndNonce(params.getOauthTimeStamp(), params.getOauthNonce());

		OAuthConsumerDAO dao = new OAuthConsumerDAO();
		secretkey = dao.getOAuthConsumerSecret(params.getOauthConsumerKey());

        if(secretkey == null) {
            log.debug("Invalid Credentials.");
            throw new AuthenticationException("Invalid Credentials.");
        }

		isValidSignature = validateOauthSignature(params, secretkey, null);

		if (!isValidSignature) {
			throw new AuthenticationException("Invalid Signature");
		}

		return generateOauthToken(params);
	}

	/**
	 * Authorizes the OAuth request token for the given scope. In order for the Consumer to be able
	 * to exchange the Request Token for an Access Token, the Consumer MUST obtain approval from the
	 * User by directing the User to the Service Provider. The Consumer constructs an HTTP GET
	 * request to the Service Provider's User Authorization URL with the following parameters.
	 * 
	 * @param params A container for the following attributes.
	 * @param params:oauth_token (required) : Request token obtained from WSO2.
	 * @param params:userName : User who authorizes the token.
	 * @param params:password : Password of the user who authorizes the token.
	 * @return oauth_token, oauth_verifier
	 * @throws Exception
	 */
	public Parameters authorizeOauthRequestToken(Parameters params) throws IdentityException, AuthenticationException {
		String tenantUser = MultitenantUtils.getTenantAwareUsername(params.getAuthorizedbyUserName());
		String domainName = MultitenantUtils.getTenantDomain(params.getAuthorizedbyUserName());
		boolean isAuthenticated = false;
        try{
            isAuthenticated = IdentityTenantUtil
                    .getRealm(domainName, params.getAuthorizedbyUserName()).getUserStoreManager()
                    .authenticate(tenantUser, params.getAuthorizedbyUserPassword());
        }catch (UserStoreException e){
            log.error(e);
            throw new IdentityException(e.getMessage(),e);
        }
		if (isAuthenticated) {
			OAuthConsumerDAO dao = new OAuthConsumerDAO();
			String oauthVerifier = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber();
			Parameters token = dao.authorizeOAuthToken(params.getOauthToken(), tenantUser,
					oauthVerifier);
			token.setOauthToken(params.getOauthToken());
			token.setOauthTokenVerifier(oauthVerifier);
			return token;
		} else {
			throw new AuthenticationException("User Authentication Failed");
		}
	}

	/**
	 * Exchanges the authorized OAuth token to an access token. To request an Access Token, the
	 * Consumer makes an HTTP request to the Service Provider's Access Token URL. The Service
	 * Provider documentation specifies the HTTP method for this request, and HTTP POST is
	 * RECOMMENDED. The request MUST be signed per Signing Requests. The Service Provider MUST
	 * ensure that: The request signature has been successfully verified. The Request Token has
	 * never been exchanged for an Access Token. The Request Token matches the Consumer Key. The
	 * verification code received from the Consumer has been successfully verified.
	 * 
	 * @param params A container for the following attributes.
	 * @param params:oauth_consumer_key (required) : Domain identifying the third-party web
	 *            application. This is the domain used when registering the application with WSO2
	 *            Identity Server.
	 * @param params:oauth_nonce (required) : Random 64-bit, unsigned number encoded as an ASCII
	 *            string in decimal format. The nonce/time-stamp pair should always be unique to
	 *            prevent replay attacks.
	 * @param params:oauth_signature_method (required) : Signature algorithm. The legal values for
	 *            this parameter "RSA-SHA1" or "HMAC-SHA1". WSO2 does not support "PLAINTEXT" and
	 *            "RSA-SHA1".
	 * @param params:oauth_signature (required) String generated using the referenced signature
	 *            method.
	 * @param params:oauth_timestamp (required) : Integer representing the time the request is sent.
	 *            The time-stamp should be expressed in number of seconds after January 1, 1970
	 *            00:00:00 GMT. scope (required) URL identifying the service(s) to be accessed. The
	 *            resulting token enables access to the specified service(s) only.
	 * @param params:oauth_version (optional) : The OAuth version used by the requesting web
	 *            application. This value must be "1.0"; if not provided, WSO2 assumes version 1.0
	 *            is in use.
	 * @return oauth_token, oauth_token_secret
	 * @throws Exception
	 */
	public Parameters getAccessToken(Parameters params) throws IdentityOAuthAdminException, AuthenticationException{

		boolean isValidSignature = false;
		String secretKey = null;

		OAuthConsumerDAO dao = new OAuthConsumerDAO();
		secretKey = dao.getOAuthConsumerSecret(params.getOauthConsumerKey());

        if(secretKey == null) {
            log.debug("Invalid Credentials.");
            throw new AuthenticationException("Invalid Credentials.");
        }

        String tokenSecret = dao.getOAuthTokenSecret(params.getOauthToken(), false);

		isValidSignature = validateOauthSignature(params, secretKey, tokenSecret);

		if (!isValidSignature) {
			throw new AuthenticationException("Invalid Signature");
		}

		// The request signature has been successfully verified

		Parameters resp = dao.getRequestToken(params.getOauthToken());

		if (resp.getOauthTokenVerifier() == null
				|| !resp.getOauthTokenVerifier().equals(params.getOauthTokenVerifier())
				|| resp.getAuthorizedbyUserName() == null) {
			throw new AuthenticationException("Invalid request for OAuth access token");
		}

		// The Request Token has never been exchanged for an Access Token resp.isAccessTokenIssued()
		// = false

		// The verification code received from the Consumer has been successfully verified -
		// resp.getOauthTokenVerifier()

		String oauthToken = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber();
		String oauthSecret = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber();

		dao.issueAccessToken(params.getOauthConsumerKey(), oauthToken, oauthSecret,
				params.getOauthToken(), resp.getAuthorizedbyUserName(), resp.getScope());
		resp.setOauthToken(oauthToken);
		resp.setOauthTokenSecret(oauthSecret);
		return resp;
	}

	/**
	 * Returns the scope and the web application this particular token been issued to.
	 * 
	 * @param oauthToken OAuth request token.
	 * @return
	 * @throws Exception
	 */
	public Parameters getScopeAndAppName(String oauthToken) throws Exception {
        OAuthConsumerDAO consumerDAO = new OAuthConsumerDAO();
        Parameters params = consumerDAO.getRequestToken(oauthToken);

        OAuthAppDAO appDAO = new OAuthAppDAO();
        OAuthAppDO oauthAppDO = appDAO.getAppInformation(params.getOauthConsumerKey());
        
        Parameters resp = new Parameters();
		resp.setScope(params.getScope());
		resp.setAppName(oauthAppDO.getApplicationName());

		return resp;
	}

	/**
	 * Validates the request to a resource protected with OAuth. After successfully receiving the
	 * Access Token and Token Secret, the Consumer is able to access the Protected Resources on
	 * behalf of the User.
	 * 
	 * @param params A container for the following attributes.
	 * @param params:oauth_consumer_key (required) : Domain identifying the third-party web
	 *            application. This is the domain used when registering the application with WSO2
	 *            Identity Server.
	 * @param params:oauth_token (required) : OAuth access token.
	 * @param params:oauth_nonce (required) : Random 64-bit, unsigned number encoded as an ASCII
	 *            string in decimal format. The nonce/time-stamp pair should always be unique to
	 *            prevent replay attacks.
	 * @param params:oauth_signature_method (required) : Signature algorithm. The legal values for
	 *            this parameter "RSA-SHA1" or "HMAC-SHA1". WSO2 does not support "PLAINTEXT" and
	 *            "RSA-SHA1".
	 * @param params:oauth_signature (required) String generated using the referenced signature
	 *            method.
	 * @param params:oauth_timestamp (required) : Integer representing the time the request is sent.
	 *            The time-stamp should be expressed in number of seconds after January 1, 1970
	 *            00:00:00 GMT. scope (required) URL identifying the service(s) to be accessed. The
	 *            resulting token enables access to the specified service(s) only.
	 * @param params:oauth_version (optional) : The OAuth version used by the requesting web
	 *            application. This value must be "1.0"; if not provided, WSO2 assumes version 1.0
	 *            is in use.
	 * @return Parameters : authorized user : the user to whom the token is issued.
     * @return Parameters : scope : the authorized scope
	 * @throws Exception Error when validating the access token request.
	 */
	public Parameters validateAuthenticationRequest(Parameters params) throws AuthenticationException, IdentityException{

		boolean isAuthenticated = false;
		String secretKey = null;

		validateTimestampAndNonce(params.getOauthTimeStamp(), params.getOauthNonce());

		OAuthConsumerDAO dao = new OAuthConsumerDAO();
		secretKey = dao.getOAuthConsumerSecret(params.getOauthConsumerKey());
        if(secretKey == null) {
            log.debug("Invalid Credentials.");
            throw new AuthenticationException("Invalid Credentials.");
        }

        String tokenSecret = dao.getOAuthTokenSecret(params.getOauthToken(), true);

		isAuthenticated = validateOauthSignature(params, secretKey, tokenSecret);

		if (isAuthenticated) {
			// Signature is verified - so this is a valid OAuth consumer.
			String subject = dao.validateAccessToken(params.getOauthConsumerKey(),
					params.getOauthToken(), params.getScope());
			Parameters returnParams = new Parameters();
            returnParams.setAuthorizedbyUserName(subject);
            returnParams.setScope(params.getScope());
            return returnParams;
		} else {
			throw new AuthenticationException("Invalid Signature.");
		}
	}

	/**
	 * @param oauthParams
	 * @return
	 * @throws RegistryException
	 * @throws IdentityException
	 */
	private Parameters generateOauthToken(Parameters oauthParams) throws IdentityOAuthAdminException {

		OAuthConsumerDAO dao = new OAuthConsumerDAO();

		String oauthToken = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber();
		String oauthSecret = org.wso2.carbon.identity.oauth.OAuthUtil.getRandomNumber();

		dao.createOAuthRequestToken(oauthParams.getOauthConsumerKey(), oauthToken, oauthSecret,
                oauthParams.getOauthCallback(), oauthParams.getScope());

        Parameters params = new Parameters();
		params.setOauthConsumerKey(oauthParams.getOauthConsumerKey());
		params.setOauthToken(oauthToken);
		params.setOauthTokenSecret(oauthSecret);

		return params;
	}

	/**
	 * @param oauthParams
	 * @param secretKey
	 * @return
	 * @throws Exception
	 */
	private boolean validateOauthSignature(OAuthConsumerDTO oauthParams, String secretKey)
			throws AuthenticationException {

		GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
		oauthParameters.setOAuthConsumerKey(oauthParams.getOauthConsumerKey());
		oauthParameters.setOAuthConsumerSecret(secretKey);
		oauthParameters.setOAuthNonce(oauthParams.getOauthNonce());
		oauthParameters.setOAuthTimestamp(oauthParams.getOauthTimeStamp());
		oauthParameters.setOAuthSignatureMethod(oauthParams.getOauthSignatureMethod());

		validateTimestampAndNonce(oauthParams.getOauthTimeStamp(), oauthParams.getOauthNonce());

		OAuthHmacSha1Signer signer = new OAuthHmacSha1Signer();
        String signature;
        try{
            String baseString = OAuthUtil.getSignatureBaseString(oauthParams.getBaseString(),
                    oauthParams.getHttpMethod(), oauthParameters.getBaseParameters());
            signature = signer.getSignature(baseString, oauthParameters);
        }catch (OAuthException e){
            throw new AuthenticationException(e.getMessage(),e);
        }

		if (signature != null
				&& URLEncoder.encode(signature).equals(oauthParams.getOauthSignature())) {
			return true;
		} else if (signature != null && signature.equals(oauthParams.getOauthSignature())) {
			return true;
		}
		return false;
	}

	/**
	 * @param oauthParams
	 * @param secretKey
	 * @return
	 * @throws Exception
	 */
	private boolean validateOauthSignature(Parameters oauthParams, String secretKey, String tokenSecret)
		throws AuthenticationException {

		GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
		oauthParameters.setOAuthConsumerKey(oauthParams.getOauthConsumerKey());
		oauthParameters.setOAuthConsumerSecret(secretKey);
		oauthParameters.setOAuthNonce(oauthParams.getOauthNonce());
		oauthParameters.setOAuthTimestamp(oauthParams.getOauthTimeStamp());
		oauthParameters.setOAuthSignatureMethod(oauthParams.getOauthSignatureMethod());

		if (oauthParams.getOauthToken() != null) {
			oauthParameters.setOAuthToken(oauthParams.getOauthToken());
		}

		if (oauthParams.getOauthTokenVerifier() != null) {
			oauthParameters.setOAuthVerifier((oauthParams.getOauthTokenVerifier()));
		}

		if (tokenSecret != null) {
			oauthParameters.setOAuthTokenSecret(tokenSecret);
		}

		OAuthHmacSha1Signer signer = new OAuthHmacSha1Signer();
        String signature;
        try{
            String baseString = OAuthUtil.getSignatureBaseString(oauthParams.getBaseString(),
                    oauthParams.getHttpMethod(), oauthParameters.getBaseParameters());
            signature = signer.getSignature(baseString, oauthParameters);
        }catch (OAuthException e){
            throw new AuthenticationException("Error while validating signature");
        }

		if (signature != null
				&& URLEncoder.encode(signature).equals(oauthParams.getOauthSignature())) {
			return true;
		} else if (signature != null && signature.equals(oauthParams.getOauthSignature())) {
			return true;
		}
		return false;
	}

	/**
	 * Unless otherwise specified by the Service Provider, the time-stamp is expressed in the number
	 * of seconds since January 1, 1970 00:00:00 GMT. The time-stamp value MUST be a positive
	 * integer and MUST be equal or greater than the time-stamp used in previous requests. The
	 * Consumer SHALL then generate a Nonce value that is unique for all requests with that
	 * timestamp. A nonce is a random string, uniquely generated for each request. The nonce allows
	 * the Service Provider to verify that a request has never been made before and helps prevent
	 * replay attacks when requests are made over a non-secure channel (such as HTTP).
	 * 
	 * @param timestamp
	 * @param nonce
	 * @throws Exception
	 */
	private void validateTimestampAndNonce(String timestamp, String nonce) throws AuthenticationException{
		if (timestamp == null || nonce == null || nonce.trim().length() == 0) {
			// We are not going to give out the exact error why the request failed.
			throw new AuthenticationException("Invalid request for OAuth access token");
		}

		long time = Long.parseLong(timestamp);

		synchronized (this) {
			long latestTimeStamp = 0;
			String strTimestamp;
			ServiceContext context = MessageContext.getCurrentMessageContext().getServiceContext();

			if ((strTimestamp = (String) context.getProperty(OAUTH_LATEST_TIMESTAMP)) != null) {
				latestTimeStamp = Long.parseLong(strTimestamp);
			}

			if (time < 0 || time < latestTimeStamp) {
				// The time-stamp value MUST be a positive integer and MUST be equal or greater than
				// the time-stamp used in previous requests
				throw new AuthenticationException("Invalid timestamp");
			}
			context.setProperty(OAUTH_LATEST_TIMESTAMP, String.valueOf(time));

			List<String> nonceStore = null;

			if ((nonceStore = (List<String>) context.getProperty(OAUTH_NONCE_STORE)) != null) {
				if (nonceStore.contains(nonce)) {
					// We are not going to give out the exact error why the request failed.
					throw new AuthenticationException("Invalid request for OAuth access token");
				} else {
					nonceStore.add(nonce);
				}
			} else {
				nonceStore = new ArrayList<String>();
				nonceStore.add(nonce);
				context.setProperty(OAUTH_NONCE_STORE, nonceStore);
			}
		}

	}

	/**
	 * @param consumerKey Consumer Key provided by the user
	 * @return consumer secret
	 * @throws Exception Error when reading the consumer secret from the persistence store.
	 */
	private String getOAuthSecretKey(String consumerKey) throws IdentityOAuthAdminException {
        OAuthConsumerDAO dao = new OAuthConsumerDAO();
        return dao.getOAuthConsumerSecret(consumerKey);
	}

}
